package web.utils;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.*;

/**
 * Created by Ace Yin on 2015/5/29 0029.
 */
public final class SpringUrlMappingUtil {

    public static Collection<Mapping> getMapping(ApplicationContext context) {

        Map<String, String> classLevelMapping = new HashMap<>();
        Map<String, List<Mapping>> methodLevelMapping = new HashMap<>();


        Map<String, HandlerMapping> allRequestMappings = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);

        for (HandlerMapping handlerMapping : allRequestMappings.values()) {
            // process the mapping defined on class
            if (handlerMapping instanceof BeanNameUrlHandlerMapping) {
                BeanNameUrlHandlerMapping mapping = (BeanNameUrlHandlerMapping) handlerMapping;
                Map<String, Object> handlerMap = mapping.getHandlerMap();
                for (Map.Entry<String, Object> entry : handlerMap.entrySet()) {
                    String url = entry.getKey();
                    Class<?> clazz = entry.getValue().getClass();
                    classLevelMapping.put(clazz.getName(), url);
                }
            }
            // process the mapping defined on method
            else if (handlerMapping instanceof RequestMappingHandlerMapping) {
                RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) handlerMapping;

                Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
                for (Map.Entry<RequestMappingInfo, HandlerMethod> requestMappingInfoHandlerMethodEntry : handlerMethods.entrySet()) {

                    RequestMappingInfo requestMappingInfo = requestMappingInfoHandlerMethodEntry.getKey();

                    String requestType = getHttpMethod(requestMappingInfo);
                    String requestUrl = getUrlMappings(requestMappingInfoHandlerMethodEntry);

                    HandlerMethod handlerMethod = requestMappingInfoHandlerMethodEntry.getValue();
                    String className = handlerMethod.getBeanType().getName();
                    String methodName = handlerMethod.getMethod().getName();
                    Class<?>[] params = handlerMethod.getMethod().getParameterTypes();

                    MethodParameter returnType = handlerMethod.getReturnType();
                    String returnTypeName = returnType.getMethod().getReturnType().getName();

                    Mapping item = new Mapping();
                    item.setClazz(className);
                    item.setHttpMethod(requestType);
                    item.setMethod(methodName);
                    item.setParams(params);
                    item.setReturns(returnTypeName);
                    item.setUrl(requestUrl);

                    List<Mapping> mappings = methodLevelMapping.get(className);
                    if (mappings == null) {
                        mappings = new ArrayList<>();
                    }
                    mappings.add(item);
                    methodLevelMapping.put(className, mappings);
                }
            }
            // ignore static resource mapping
            else {
                continue;
            }
        }
        // concat class level url and method level url mapping
        for (String clazz : classLevelMapping.keySet()) {
            String clazzLevelUrl = classLevelMapping.get(clazz);
            List<Mapping> mappings = methodLevelMapping.get(clazz);
            if (mappings != null && mappings.size() > 0) {
                for (Mapping mapping : mappings) {
                    mapping.setBaseUri(clazzLevelUrl);
                    if (clazzLevelUrl.endsWith("*")) {
                        clazzLevelUrl = clazzLevelUrl.substring(0, clazzLevelUrl.lastIndexOf("*"));
                    }
                    mapping.setUrl(clazzLevelUrl + mapping.getUrl());
                }
            }
        }

        Collection<List<Mapping>> values = methodLevelMapping.values();
        List<Mapping> result = new ArrayList<>();

        values.forEach(item -> result.addAll(item));

        return result;
    }


    private static String getUrlMappings(Map.Entry<RequestMappingInfo, HandlerMethod> requestMappingInfoHandlerMethodEntry) {
        RequestMappingInfo requestMappingInfo = requestMappingInfoHandlerMethodEntry.getKey();
        HandlerMethod handlerMethod = requestMappingInfoHandlerMethodEntry.getValue();

        PatternsRequestCondition patternsCondition = requestMappingInfo.getPatternsCondition();
        String requestUrl = "";

        Set<String> patterns = patternsCondition.getPatterns();
        for (String pattern : patterns) {
            requestUrl += pattern + " ";
        }
        if ("".equals(requestUrl)) {
            requestUrl = handlerMethod.getMethod().getName();
        }
        return requestUrl;
    }

    private static String getHttpMethod(RequestMappingInfo requestMappingInfo) {
        String httpMethod = "";
        RequestMethodsRequestCondition methodCondition = requestMappingInfo.getMethodsCondition();
        Set<RequestMethod> methods = methodCondition.getMethods();

        if (methods == null || methods.isEmpty()) {
            for (RequestMethod method : methods) {
                httpMethod += method.name() + " ";
            }
        } else {
            httpMethod = "*";
        }
        return httpMethod;
    }


    public static class Mapping {
        @JsonIgnore
        public String baseUri;
        @JsonProperty("http-method")
        public String httpMethod;
        public String url;
        @JsonProperty("class")
        public String clazz;
        public String method;
        @JsonProperty("return")
        public String returns;
        public Class<?>[] params;

        public String getBaseUri() {
            return baseUri;
        }

        public void setBaseUri(String baseUri) {
            this.baseUri = baseUri;
        }

        public String getHttpMethod() {
            return httpMethod;
        }

        public void setHttpMethod(String httpMethod) {
            this.httpMethod = httpMethod;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getClazz() {
            return clazz;
        }

        public void setClazz(String clazz) {
            this.clazz = clazz;
        }

        public String getMethod() {
            return method;
        }

        public void setMethod(String method) {
            this.method = method;
        }

        public String getReturns() {
            return returns;
        }

        public void setReturns(String returns) {
            this.returns = returns;
        }

        public Class<?>[] getParams() {
            return params;
        }

        public void setParams(Class<?>[] params) {
            this.params = params;
        }
    }
}
